/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package org.atomojo.app.db.sparql;

import java.net.URI;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.logging.Logger;
import org.atomojo.app.Nil;
import org.atomojo.app.db.DB;
import org.atomojo.app.db.Entry;
import org.atomojo.app.db.Feed;
import org.atomojo.app.db.Term;
import org.atomojo.app.db.TermInstance;
import org.atomojo.sparql.Expression;
import org.atomojo.sparql.LiteralRef;
import org.atomojo.sparql.NilRef;
import org.atomojo.sparql.Query;
import org.atomojo.sparql.Ref;
import org.atomojo.sparql.RelationalExpression;
import org.atomojo.sparql.SelectQuery;
import org.atomojo.sparql.TermRef;
import org.atomojo.sparql.Triple;
import org.atomojo.sparql.VariableRef;

/**
 *
 * @author alex
 */
public class QueryContext {

   static final URI FEED_TERM = URI.create("http://www.atomojo.org/O/type/feed");
   static final URI ENTRY_TERM = URI.create("http://www.atomojo.org/O/type/entry");
   
   public interface ResultListener {
      void onStart()
         throws QueryException;
      void onEnd()
         throws QueryException;
      void onEntry(Entry entry)
         throws QueryException;
      void onFeed(Feed feed)
         throws QueryException;
   }
   
   static class Value {
      Term term;
      Object value;
      Value(Term term,Object value)
      {
         this.term = term;
         this.value = value;
      }
   }

   Logger log;
   DB db;
   Query query;
   boolean entryPivot;
   boolean feedPivot;
   boolean unsatisfiable;
   String var;
   Triple pivot;
   URI pivotTerm;
   Object pivotValue;
   public QueryContext(Logger log,DB db,Query query) 
      throws QueryException
   {
      this.log = log;
      this.db = db;
      this.query = query;
      if (!(query instanceof SelectQuery)) {
         throw new QueryException("Only select queries are supported.");
      }
      SelectQuery select = (SelectQuery)query;
      Set<String> returnVars = select.getReturnVariables();
      if (returnVars.size()!=1) {
         throw new QueryException("Only one entry/feed return variable is currently supported.");
      }
      var = returnVars.iterator().next();
      List<Triple> triples = select.getWhereClause().getClauses();
      pivot = null;
      entryPivot = false;
      feedPivot = false;
      unsatisfiable = false;
      Triple lastBind = null;
      for (Triple t : triples) {
         Ref subject = t.getSubject();
         Ref predicate = t.getPredicate();
         Ref object = t.getObject();
         if (subject instanceof VariableRef && predicate instanceof TermRef && object instanceof NilRef) {
            String name = ((VariableRef)subject).getName();
            if (name.equals(var)) {
               URI term = ((TermRef)predicate).getTerm();
               if (term.equals(ENTRY_TERM)) {
                  entryPivot = true;
               } else if (term.equals(FEED_TERM)) {
                  feedPivot = true;
               } else if (pivot==null) {
                  pivot = t;
                  pivotTerm = term;
                  log.info("Pivoting on "+pivot);
                  pivotValue = pivot.getObject() instanceof LiteralRef ? ((LiteralRef)pivot.getObject()).getValue() : Nil.getInstance();
               }
            } else {
               unsatisfiable = true;
            }
         } else if (subject instanceof TermRef) {
            // should be ok (e.g. find a specific entry/feed)
            log.info("Unsatisfiable triple "+t);
            unsatisfiable = true;
         } else if (subject instanceof VariableRef && predicate instanceof TermRef && object instanceof LiteralRef) {
            // ok
            // limit to var now
            String name = ((VariableRef)subject).getName();
            if (!name.equals(var)) {
               log.info("Unsatisfiable triple "+t);
               unsatisfiable = true;
            } else if (pivot==null) {
               pivot = t;
               pivotTerm = ((TermRef)predicate).getTerm();
               log.info("Pivoting on "+pivot);
               pivotValue = pivot.getObject() instanceof LiteralRef ? ((LiteralRef)pivot.getObject()).getValue() : Nil.getInstance();
            }
         } else if (subject instanceof VariableRef && predicate instanceof TermRef && object instanceof VariableRef) {
            // ok
            // limit to var now
            String name = ((VariableRef)subject).getName();
            if (!name.equals(var)) {
               log.info("Unsatisfiable triple "+t);
               unsatisfiable = true;
            } else {
               lastBind = t;
            }
         } else {
            // unsatisfiable
            log.info("Unsatisfiable triple "+t);
            unsatisfiable = true;
         }
      }
      if (entryPivot && feedPivot) {
         log.info("Entry and feed predicates on same variable is unsatisfiable.");
         unsatisfiable = true;
      }
      if (!unsatisfiable && pivot==null) {
         pivot = lastBind;
         pivotTerm = ((TermRef)lastBind.getPredicate()).getTerm();
         log.info("Pivoting on "+pivot);
         pivotValue = pivot.getObject() instanceof LiteralRef ? ((LiteralRef)pivot.getObject()).getValue() : Nil.getInstance();
      }
   }
   
   public void execute(ResultListener listener)
      throws QueryException
   {
      if (unsatisfiable) {
         listener.onStart();
         listener.onEnd();
         return;
      }
      try {
         List<Triple> triples = query.getWhereClause().getClauses();
         if (triples.size()==0) {
            listener.onStart();
            Iterator<Feed> feeds = db.getFeeds();
            while (feeds.hasNext()) {
               listener.onFeed(feeds.next());
            }
            Iterator<Entry> entries = db.getEntries();
            while (entries.hasNext()) {
               listener.onEntry(entries.next());
            }
            listener.onEnd();
         } else if (entryPivot && triples.size()==1) {
            listener.onStart();
            Iterator<Entry> entries = db.getEntries();
            while (entries.hasNext()) {
               listener.onEntry(entries.next());
            }
            listener.onEnd();
         } else if (feedPivot && triples.size()==1) {
            listener.onStart();
            Iterator<Feed> feeds = db.getFeeds();
            while (feeds.hasNext()) {
               listener.onFeed(feeds.next());
            }
            listener.onEnd();
         } else {
            if (!entryPivot && !feedPivot) {
               // We'll query both feeds and entries
               entryPivot = true;
               feedPivot = true;
            }

            boolean skip = false;
            Map<String,Term> bindings = new TreeMap<String,Term>();
            List<Value> comparisons = new ArrayList<Value>(); 
            for (Triple triple : query.getWhereClause().getClauses()) {
               if (triple==pivot) {
                  continue;
               }
               URI uri = ((TermRef)triple.getPredicate()).getTerm();
               Term term = db.findTerm(uri);
               Ref object = triple.getObject();
               if (object instanceof VariableRef) {
                  if (term!=null) {
                     bindings.put(((VariableRef)object).getName(),term);
                  }
               } else if (object instanceof LiteralRef) {
                  if (term==null) {
                     skip = true;
                  } else {
                     comparisons.add(new Value(term,((LiteralRef)object).getValue()));
                  }
               } else if (object instanceof NilRef) {
                  if (term==null) {
                     skip = true;
                  } else {
                     comparisons.add(new Value(term,Nil.getInstance()));
                  }
               }
            }
            listener.onStart();
            if (!skip) {
               Term term = db.findTerm(pivotTerm);
               if (term!=null) {
                  if (feedPivot) {
                     Iterator<TermInstance<Feed>> feeds = db.getFeedsByTerm(term,pivotValue);
                     while (feeds.hasNext()) {
                        TermInstance<Feed> termValue = feeds.next();
                        // need to compare if nil
                        if (pivotValue==Nil.getInstance() && termValue.getValue()!=Nil.getInstance()) {
                           continue;
                        }
                        Feed feed = termValue.getTarget();
                        Map<String,Object> variables = new TreeMap<String,Object>();
                        boolean ok = true;
                        // match values
                        for (Triple triple : query.getWhereClause().getClauses()) {
                           if (triple==pivot) {
                              continue;
                           }
                           for (Value value : comparisons) {
                              TermInstance<Feed> boundValue = feed.getTerm(value.term);
                              if (boundValue==null) {
                                 ok = false;
                                 break;
                              } else if (value.value==null && boundValue.getValue()!=null) {
                                 ok = false;
                                 break;
                              } else if (value.value!=null && !value.value.equals(boundValue.getValue())) {
                                 ok = false;
                                 break;
                              }
                           }
                           if (!ok) {
                              break;
                           }
                           for (String name : bindings.keySet()) {
                              Term t = bindings.get(name);
                              TermInstance<Feed> boundValue = feed.getTerm(t);
                              if (boundValue!=null && boundValue.getValue()!=null) {
                                 variables.put(name,boundValue.getValue());
                              }
                           }
                        }
                        // do filter
                        ok = ok ? filter(variables,query.getWhereClause().getFilterExpressions()) : false;
                        if (ok) {
                           listener.onFeed(feed);
                        }
                     }
                  }
                  if (entryPivot) {
                     // TODO: code is almost the same as feed
                     Iterator<TermInstance<Entry>> entries = db.getEntriesByTerm(term,pivotValue);
                     while (entries.hasNext()) {
                        TermInstance<Entry> termValue = entries.next();
                        // need to compare if nil
                        if (pivotValue==Nil.getInstance() && termValue.getValue()!=Nil.getInstance()) {
                           continue;
                        }
                        Entry entry = termValue.getTarget();
                        Map<String,Object> variables = new TreeMap<String,Object>();
                        boolean ok = true;
                        // match values
                        for (Triple triple : query.getWhereClause().getClauses()) {
                           if (triple==pivot) {
                              continue;
                           }
                           for (Value value : comparisons) {
                              TermInstance<Entry> boundValue = entry.getTerm(value.term);
                              if (boundValue==null) {
                                 ok = false;
                                 break;
                              } else if (value.value==null && boundValue.getValue()!=null) {
                                 ok = false;
                                 break;
                              } else if (value.value!=null && !value.value.equals(boundValue.getValue())) {
                                 ok = false;
                                 break;
                              }
                           }
                           if (!ok) {
                              break;
                           }
                           for (String name : bindings.keySet()) {
                              Term t = bindings.get(name);
                              TermInstance<Entry> boundValue = entry.getTerm(t);
                              if (boundValue!=null && boundValue.getValue()!=null) {
                                 variables.put(name,boundValue.getValue());
                              }
                           }
                        }
                        // do filter
                        ok = ok ? filter(variables,query.getWhereClause().getFilterExpressions()) : false;
                        if (ok) {
                           listener.onEntry(entry);
                        }
                     }
                  }
               } else {
                  log.info("Could not find pivot term "+pivotTerm);
               }
            }
            listener.onEnd();
         }
      } catch (SQLException ex) {
         throw new QueryException("DB exception while running query.",ex);
      }
   }
   
   boolean filter(Map<String,Object> variables,List<Expression> expressions)
   {
      boolean ok = true;
      for (Expression expr : expressions) {
         if (expr instanceof RelationalExpression) {
            RelationalExpression r = (RelationalExpression)expr;
            Object leftSide = null;
            Object rightSide = null;
            if (r.getLeftSide() instanceof VariableRef) {
               leftSide = variables.get(((VariableRef)r.getLeftSide()).getName());
            } else {
               leftSide = ((LiteralRef)r.getLeftSide()).getValue();
            }
            if (r.getRightSide() instanceof VariableRef) {
               rightSide = variables.get(((VariableRef)r.getRightSide()).getName());
            } else {
               rightSide = ((LiteralRef)r.getRightSide()).getValue();
            }
            if (rightSide==null || leftSide==null) {
               ok = false;
               break;
            } else {
               switch (r.getOperator()) {
                  case EQUALS:
                     ok = leftSide.equals(rightSide);
                     break;
                  case NOT_EQUALS:
                     ok = !leftSide.equals(rightSide);
                     break;
               }
            }
         } else {
            ok = false;
            break;
         }
      }
      return ok;
   }
   
   
}
